package net.airvantage.sched.services.impl;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.math.NumberUtils;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.ObjectAlreadyExistsException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.airvantage.sched.app.exceptions.AppException;
import net.airvantage.sched.dao.JobConfigDao;
import net.airvantage.sched.dao.JobLockDao;
import net.airvantage.sched.dao.JobSchedulingDao;
import net.airvantage.sched.dao.JobWakeupDao;
import net.airvantage.sched.model.JobConfig;
import net.airvantage.sched.model.JobDef;
import net.airvantage.sched.model.JobLock;
import net.airvantage.sched.model.JobScheduling;
import net.airvantage.sched.model.JobSchedulingType;
import net.airvantage.sched.model.JobState;
import net.airvantage.sched.model.JobWakeup;
import net.airvantage.sched.quartz.job.CronJob;
import net.airvantage.sched.quartz.job.WakeupJob;
import net.airvantage.sched.services.JobSchedulingService;
import net.airvantage.sched.services.JobStateService;
/**
* A service to manage the jobs scheduling.
*/
public class JobSchedulingServiceImpl implements JobSchedulingService {
private Logger LOG = LoggerFactory.getLogger(JobSchedulingServiceImpl.class);
private Scheduler scheduler;
private JobStateService jobStateService;
private JobLockDao jobLockDao;
private JobConfigDao jobConfigDao;
private JobWakeupDao jobWakeupDao;
private JobSchedulingDao jobSchedulingDao;
private String jobWakeupCron;
// ------------------------------------------------ Constructors --------------------------------------------------
public JobSchedulingServiceImpl(Scheduler scheduler, JobStateService jobStateService, JobConfigDao jobConfigDao,
JobLockDao jobLockDao, JobSchedulingDao jobSchedulingDao, JobWakeupDao jobWakeupDao, String jobWakeupCron) {
this.scheduler = scheduler;
this.jobStateService = jobStateService;
this.jobLockDao = jobLockDao;
this.jobConfigDao = jobConfigDao;
this.jobWakeupDao = jobWakeupDao;
this.jobSchedulingDao = jobSchedulingDao;
this.jobWakeupCron = jobWakeupCron;
}
public void loadInternalJobs() throws AppException {
try {
JobDef jobDef = new JobDef();
JobConfig jobConfig = new JobConfig();
jobDef.setConfig(jobConfig);
jobConfig.setId("internal/wakeup-jobs-timer");
JobScheduling jobScheduling = new JobScheduling();
jobDef.setScheduling(jobScheduling);
jobScheduling.setType(JobSchedulingType.CRON);
jobScheduling.setValue(jobWakeupCron);
scheduleQuarzJob(jobDef, WakeupJob.class);
} catch (Exception ex) {
LOG.error("Unable to load internal jobs", ex);
throw new AppException("load.internal.jobs.error", ex);
}
}
// ------------------------------------------ JobSchedulingService Methods ----------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void scheduleJob(JobDef jobDef) throws AppException {
LOG.debug("scheduleJob : jobDef={}", jobDef);
Validate.notNull(jobDef);
validate(jobDef.getConfig());
validate(jobDef.getScheduling());
try {
if (jobDef.getScheduling().getType() == JobSchedulingType.WAKEUP) {
JobWakeup wakeup = new JobWakeup();
wakeup.setId(jobDef.getConfig().getId());
wakeup.setCallback(jobDef.getConfig().getUrl());
wakeup.setWakeupTime(new Long(jobDef.getScheduling().getValue()));
jobWakeupDao.persist(wakeup);
} else {
scheduleQuarzJob(jobDef, CronJob.class);
// Persist the job configuration
this.jobConfigDao.persist(jobDef.getConfig());
if (LOG.isInfoEnabled()) {
String state = this.jobStateService.isJobLocked(jobDef.getConfig().getId()) ? "locked" : "unlocked";
LOG.info("The job {} is scheduled with CRON expression {}, current state is '{}'", jobDef
.getConfig().getId(), jobDef.getScheduling().getValue(), state);
}
}
} catch (Exception ex) {
LOG.error("Unable to schedule job " + jobDef, ex);
throw new AppException("schedule.job.error", Arrays.asList(jobDef.getConfig().getId()), ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public void rescheduleJob(String jobId, JobScheduling conf) throws AppException {
LOG.debug("rescheduleJob : id={}, conf={}", jobId, conf);
validate(conf);
try {
Trigger trigger = this.buildTrigger(jobId, conf, null);
this.scheduler.rescheduleJob(trigger.getKey(), trigger);
} catch (Exception e) {
LOG.error("Unable to re-schedule job " + jobId + " with configuration " + conf, e);
throw new AppException("reschedule.job.error", Arrays.asList(jobId), e);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean unscheduleJob(String jobId) throws AppException {
LOG.debug("unscheduleJob : jobId={}", jobId);
boolean res = false;
try {
res = unscheduleQuartzJob(jobId);
this.jobConfigDao.delete(jobId);
this.jobLockDao.delete(jobId);
} catch (Exception ex) {
LOG.error(String.format("Unable to unschedule job {}", jobId), ex);
throw new AppException("unchedule.job.error", Arrays.asList(jobId), ex);
}
return res;
}
/**
* {@inheritDoc}
*/
@Override
public void ackJob(String jobId) throws AppException {
LOG.debug("ackJob : jobId={}", jobId);
try {
JobConfig config = this.jobConfigDao.find(jobId);
if (config == null) {
throw new AppException("job.not.found", Arrays.asList(jobId));
}
JobScheduling schedConf = this.jobSchedulingDao.find(jobId);
if (schedConf == null) {
// Remove job configuration when a job is complete
this.unscheduleJob(jobId);
} else {
this.jobStateService.unlockJob(jobId);
}
} catch (AppException aex) {
throw aex;
} catch (Exception sex) {
LOG.error(String.format("Unable to acknowledge job {}", jobId), sex);
throw new AppException("ack.job.error", Arrays.asList(jobId), sex);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean triggerJob(String jobId) throws AppException {
LOG.debug("triggerJob : jobId={}", jobId);
boolean res = false;
try {
JobState jobState = this.jobStateService.find(jobId);
if (jobState == null) {
throw new AppException("job.not.found", Arrays.asList(jobId));
}
JobLock lock = jobState.getLock();
// Currently locked jobs should not be re-triggered
if (lock.isLocked() && !lock.isExpired()) {
res = false;
} else {
this.scheduler.triggerJob(this.buildJobKey(jobId));
res = true;
}
} catch (AppException aex) {
throw aex;
} catch (Exception ex) {
LOG.error(String.format("Unable to trigger job {}", jobId), ex);
throw new AppException("trigger.job.error", Arrays.asList(jobId), ex);
}
return res;
}
/**
* {@inheritDoc}
*/
@Override
public void clearJobs() throws AppException {
LOG.debug("clearJobs");
try {
// Delete configuration
this.jobLockDao.deleteAll();
this.jobConfigDao.deleteAll();
this.jobWakeupDao.deleteAll();
// Delete scheduling
this.scheduler.clear();
} catch (Exception ex) {
LOG.error("Unable to clear jobs", ex);
throw new AppException("clear.jobs.error", ex);
}
}
// ----------------------------------------------- Private Methods ------------------------------------------------
private void validate(JobConfig jobConfig) throws AppException {
Validate.notNull(jobConfig);
if (StringUtils.isEmpty(jobConfig.getId())) {
jobConfig.setId(UUID.randomUUID().toString());
}
if (StringUtils.isEmpty(jobConfig.getUrl())) {
throw new AppException("missing.callback.url");
}
if (jobConfig.getUrl().length() > 255) {
throw new AppException("too.long.url");
}
}
private void validate(JobScheduling scheduling) throws AppException {
Validate.notNull(scheduling);
if (scheduling.getType() == null) {
throw new AppException("missing.scheduling.type");
}
if (scheduling.getType() == JobSchedulingType.CRON) {
if (StringUtils.isEmpty(scheduling.getValue()))
throw new AppException("missing.schedule.value");
try {
CronExpression.validateExpression(scheduling.getValue());
} catch (ParseException e) {
throw new AppException("invalid.schedule.value", Arrays.asList(scheduling.getValue()));
}
}
if (scheduling.getType() == JobSchedulingType.WAKEUP) {
if (StringUtils.isEmpty(scheduling.getValue()))
throw new AppException("missing.schedule.value");
if (!NumberUtils.isDigits(scheduling.getValue()))
throw new AppException("invalid.schedule.value");
}
}
private void scheduleQuarzJob(JobDef jobDef, Class<? extends Job> type) throws SchedulerException {
JobDetail job = this.buildJob(jobDef.getConfig(), type);
Trigger trigger = this.buildTrigger(jobDef.getConfig().getId(), jobDef.getScheduling(), job.getKey());
// Add the new jobs
// if (!this.scheduler.checkExists(job.getKey())) {
// TMP MIGRATION - Update each time to update Job.class
this.scheduler.addJob(job, true);
// }
// Add the new triggers or update existing ones
try {
if (this.scheduler.checkExists(trigger.getKey())) {
this.scheduler.rescheduleJob(trigger.getKey(), trigger);
} else {
this.scheduler.scheduleJob(trigger);
}
} catch (ObjectAlreadyExistsException e) {
LOG.info("Trigger already exists with key {}, try to replace it.", trigger.getKey());
// To manage concurrent calls
this.scheduler.rescheduleJob(trigger.getKey(), trigger);
}
}
/**
* Try unscheduling the job.
*
* @return true if the job existed, and was unscheduled.
*/
private boolean unscheduleQuartzJob(String jobId) throws SchedulerException {
return this.scheduler.deleteJob(this.buildJobKey(jobId));
}
private TriggerKey buildTiggerKey(String confId) {
return new TriggerKey(confId, confId + "-trigger");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Trigger buildTrigger(String confId, JobScheduling conf, JobKey job) {
TriggerKey key = this.buildTiggerKey(confId);
TriggerBuilder trigger = TriggerBuilder.newTrigger().withIdentity(key);
// Set CRON value
if (StringUtils.isNotEmpty(conf.getValue())) {
trigger.withSchedule(CronScheduleBuilder.cronSchedule(conf.getValue()));
}
// Set DATE value
if (conf.getStartAt() > 0) {
trigger.startAt(new Date(conf.getStartAt()));
}
if (job != null) {
trigger.forJob(job);
}
return trigger.build();
}
private JobKey buildJobKey(String confId) {
return new JobKey(confId);
}
private JobDetail buildJob(JobConfig jobConf, Class<? extends Job> type) {
JobKey key = this.buildJobKey(jobConf.getId());
JobBuilder job = JobBuilder.newJob(type).withIdentity(key).storeDurably();
return job.build();
}
}